Conversation
Razor's source generator brackets WriteLiteral calls that populate an HTML attribute value on a tag-helper-enabled element (e.g. �sp-for) between BeginWriteTagHelperAttribute()/EndWriteTagHelperAttribute() calls. The captured text is buffered internally and HTML-attribute- encoded before being rendered, so it is not a real XSS sink, but cs/web/xss previously flagged it as one. - Add getBeginWriteTagHelperAttributeMethod() / getEndWriteTagHelperAttributeMethod() to MicrosoftAspNetCoreMvcRazorPageBase. - Add isBracketedForTagHelperAttribute() to Html.qll and use it to exclude bracketed WriteLiteral calls from MicrosoftAspNetRazorPageWriteLiteralSink, using same-basic-block, immediately-adjacent-bracket matching so unrelated bracket pairs cannot "adopt" an unbracketed call. - Add RazorTagHelperAttribute.cshtml(.g.cs) test coverage: a suppressed bracketed write, an unbracketed positive control, two independent brackets in one basic block, and a bare write sandwiched between brackets. - Add change note (majorAnalysis). Diagnosed and validated against the real customer reproduction that motivated this fix (field-security-codeql#257 / github#261): the false positive (ChangeAccountInfo.cshtml) is no longer reported, while the genuine Html.Raw-based positive control (ProfileSummary.cshtml) remains reported. Full CWE-079 test suite (6 tests) passes with no regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot Code Review correctly flagged that isBracketedForTagHelperAttribute never related the receivers of beginCall/writeLiteral/endCall, so a bracket on one page instance could theoretically be mistaken for a bracket around a WriteLiteral call on a different page (e.g. otherPage.BeginWriteTagHelperAttribute(); this.WriteLiteral(model); otherPage.EndWriteTagHelperAttribute();). Require all three calls to have an implicit his qualifier, which is how the Razor source generator always emits them, guaranteeing they act on the same page instance. Re-verified: XSS.ql compiles, all 6 CWE-079 tests pass, and the real customer reproduction database still shows the FP suppressed and the genuine Html.Raw positive control still reported. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Shortened to a single terse sentence, matching the depth/style of other recent change-notes (one bullet, no implementation detail), and called out the ASP.NET Core Razor Pages/MVC scope. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The documentation incorrectly promises downstream HTML encoding instead of stating that the call only writes to a temporary buffer.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Prevents false-positive C# XSS alerts for buffered Razor tag-helper attribute writes.
Changes:
- Models Razor tag-helper attribute buffering methods.
- Excludes correctly bracketed
WriteLiteralcalls from direct XSS sinks. - Adds regression fixtures and a change note.
| File | Description |
|---|---|
Html.qll |
Detects buffered WriteLiteral calls. |
AspNetCore.qll |
Models tag-helper buffering methods. |
RazorTagHelperAttribute.cshtml |
Provides source-map fixture. |
RazorTagHelperAttribute.cshtml.g.cs |
Adds generated-code test scenarios. |
XSS.expected |
Updates expected query results. |
| Change note | Documents the analysis change. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot Code Review correctly pointed out that bracketing between BeginWriteTagHelperAttribute()/EndWriteTagHelperAttribute() only proves the value is captured into a buffer rather than written directly to the response; it does not, by itself, guarantee that every tag helper later HTML-attribute-encodes that buffer. Reworded the doc comments and the change note to justify the exclusion on "not a direct write to the response" rather than on assumed downstream encoding. No logic change; XSS.ql compiles and the CWE-079/XSS test still passes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
michaelnebel
left a comment
There was a problem hiding this comment.
Thank you very much for doing this @felickz !
I have added a couple of minor comments.
In the description, you mention "ASP.NET Core Razor Pages reproduction project mirroring the example". Which project is that? Perhaps, we should add it to our DCA suite.
| i2 < j | ||
| ) and | ||
| not exists(int k2, Call other | | ||
| ( | ||
| other = page.getBeginWriteTagHelperAttributeMethod().getACall() or | ||
| other = page.getEndWriteTagHelperAttributeMethod().getACall() | ||
| ) and | ||
| writeLiteral.getBasicBlock().getNode(k2) = other.getControlFlowNode() and | ||
| j < k2 and | ||
| k2 < k |
There was a problem hiding this comment.
| i2 < j | |
| ) and | |
| not exists(int k2, Call other | | |
| ( | |
| other = page.getBeginWriteTagHelperAttributeMethod().getACall() or | |
| other = page.getEndWriteTagHelperAttributeMethod().getACall() | |
| ) and | |
| writeLiteral.getBasicBlock().getNode(k2) = other.getControlFlowNode() and | |
| j < k2 and | |
| k2 < k | |
| i2 < k |
Shouldn't this suffice as well?
The difference is that we don't check that there is both another begin/end between in ]i-j[ AND ]j-k[, but we only check there is no other begin/end is the most narrow begin/end around WriteLiteral.
| endCall.hasImplicitThisQualifier() and | ||
| writeLiteral.getBasicBlock().getNode(i) = beginCall.getControlFlowNode() and | ||
| writeLiteral.getBasicBlock().getNode(j) = writeLiteral.getControlFlowNode() and | ||
| writeLiteral.getBasicBlock().getNode(k) = endCall.getControlFlowNode() and |
There was a problem hiding this comment.
Perhaps introduce a BasicBlock bb as a part of the exists with bb = writeLiteral.getBasicBlock(), then it is a bit easier to see that no other basic blocks are used by mistake.


Summary
cs/web/xsscurrently flagsRazorPageBase.WriteLiteral(...)calls that the Razor source generator emits for the value of an HTML attribute on an element that also carries a tag helper (for example,asp-for="Model.Something"). This is a false positive: the Razor codegen brackets these calls betweenBeginWriteTagHelperAttribute()/EndWriteTagHelperAttribute(), which capture the value into an internal buffer rather than writing it directly to the response. It is not a real XSS sink.Example
Given a controller action that binds user-provided input to a view model:
and a Razor Pages/MVC view that renders one of its properties through a tag helper attribute:
cs/web/xsspreviously reported:This is a false positive. The value assigned to the
asp-forattribute is captured into an internal string buffer via matchingBeginWriteTagHelperAttribute()/EndWriteTagHelperAttribute()calls generated around theWriteLiteralcall, so it becomes a tag helper attribute value rather than being written directly to the response as page markup.Fix
AspNetCore.qll: addgetBeginWriteTagHelperAttributeMethod()/getEndWriteTagHelperAttributeMethod()toMicrosoftAspNetCoreMvcRazorPageBase.Html.qll: addisBracketedForTagHelperAttribute()and use it to exclude bracketedWriteLiteralcalls fromMicrosoftAspNetRazorPageWriteLiteralSink. The predicate requiresbeginCall,writeLiteral,endCallto appear (in that order) in the same basic block, with no otherBegin/EndWriteTagHelperAttributecall in between on either side, and all three calls to share an implicitthisreceiver, so an unrelated bracket (on this or another page instance) can't "adopt" an unbracketed call.Test coverage
Added
RazorTagHelperAttribute.cshtml/.cshtml.g.csto the existingSecurity Features/CWE-079/XSStest, covering:WriteLiteral(model)(must not alert, suppressed FP).WriteLiteral(model)(must alert, positive control).WriteLiteral(model)sandwiched between two unrelated brackets (must still alert).Full
Security Features/CWE-079suite (6 tests) passes with no regressions. Also validated end-to-end against a small standalone ASP.NET Core Razor Pages reproduction project mirroring the example above: the false positive is no longer reported, while a genuineHtml.Raw-based positive control in the same project remains reported.Change note
Added
csharp/ql/lib/change-notes/2026-09-18-razor-tag-helper-attribute-xss-fp.md(category: majorAnalysis).